package com.loeng.module.infra.framework.file.core.client.s3;

import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil;
import com.loeng.module.infra.framework.file.core.client.AbstractFileClient;
import lombok.SneakyThrows;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.core.ResponseInputStream;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
import software.amazon.awssdk.services.s3.model.GetObjectResponse;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
import software.amazon.awssdk.services.s3.presigner.model.PresignedPutObjectRequest;
import software.amazon.awssdk.services.s3.presigner.model.PutObjectPresignRequest;

import java.net.URI;
import java.time.Duration;

/**
 * 基于 S3 协议的文件客户端，实现 MinIO、阿里云、腾讯云、七牛云、华为云等云服务
 * <p>
 * S3 协议的客户端，采用亚马逊提供的 software.amazon.awssdk.s3 库
 *
 * @author loeng zhuo
 */
public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {

    /**
     * S3 客户端
     */
    private S3Client client;
    /**
     * 默认的 S3 区域 (自定义 中国-安徽)，避免出现异常
     */
    private final Region S3_COMMON_REGION = Region.of("cn-anhui");

    /**
     * 构造函数 (反射调用时回传的参数) {@link com.loeng.module.infra.framework.file.core.client.FileClientFactoryImpl#createFileClient}
     *
     * @param id     文件客户端编号
     * @param config 文件客户端配置
     */
    public S3FileClient(Long id, S3FileClientConfig config) {
        super(id, config);
    }

    /**
     * 初始化文件客户端
     */
    @Override
    protected void doInit() {
        // 补全 domain
        if (StrUtil.isEmpty(config.getDomain())) {
            config.setDomain(buildDomain());
        }
        // 初始化客户端
        client = S3Client.builder()
                .credentialsProvider(buildCredentials())
                .endpointOverride(buildEndpointConfiguration())
                // 显式指定区域 （必须声明 不然报错）
                .region(S3_COMMON_REGION)
                .build();
    }

    /**
     * 上传文件
     *
     * @param content 文件流
     * @param path    相对路径
     * @return 完整路径，即 HTTP 访问地址
     * @throws Exception 上传文件时，抛出 Exception 异常
     */
    @Override
    public String upload(byte[] content, String path, String type) throws Exception {
        // 执行上传 path: 相对路径  content：文件内容
        PutObjectRequest putObjectRequest = PutObjectRequest.builder()
                .bucket(config.getBucket())
                .key(path)
                .contentType(type)
                .contentLength((long) content.length)
                .build();
        client.putObject(putObjectRequest, RequestBody.fromBytes(content));
        // 拼接返回路径
        return config.getDomain() + "/" + path;
    }

    /**
     * 删除文件
     *
     * @param path 相对路径
     * @throws Exception 删除文件时，抛出 Exception 异常
     */
    @Override
    public void delete(String path) throws Exception {
        // 构建删除对象
        DeleteObjectRequest deleteObjectRequest = DeleteObjectRequest.builder()
                .bucket(config.getBucket())
                .key(path)
                .build();
        client.deleteObject(deleteObjectRequest);
    }

    /**
     * 获得文件的内容
     *
     * @param path 相对路径
     * @return 文件的内容
     */
    @Override
    public byte[] getContent(String path) throws Exception {
        // 构建请求对象
        GetObjectRequest getObjectRequest = GetObjectRequest.builder()
                .bucket(config.getBucket())
                .key(path)
                .build();
        ResponseInputStream<GetObjectResponse> response = client.getObject(getObjectRequest);
        return IoUtil.readBytes(response);
    }

    /**
     * 获得文件预签名地址
     *
     * @param path 相对路径
     * @return 文件预签名地址
     */
    @Override
    @SneakyThrows
    public FilePreSignedUrlRespDTO getPreSignedObjectUrl(String path) throws Exception {
        // 构建预签名对象
        S3Presigner presigner = S3Presigner.builder()
                .credentialsProvider(buildCredentials())
                .endpointOverride(buildEndpointConfiguration())
                .region(S3_COMMON_REGION)
                .build();
        // 构建预签名请求
        PutObjectPresignRequest presignRequest = PutObjectPresignRequest.builder()
                .signatureDuration(Duration.ofMinutes(10))
                .putObjectRequest(PutObjectRequest.builder()
                        .bucket(config.getBucket())
                        .key(path)
                        .build())
                .build();
        // 获取预签名地址
        PresignedPutObjectRequest presignedRequest = presigner.presignPutObject(presignRequest);
        // 构建返回结果
        return FilePreSignedUrlRespDTO.builder()
                .uploadUrl(presignedRequest.url().toString())
                .url(config.getDomain() + "/" + path)
                .build();
    }

    /**
     * 构建 domain，如果配置了 domain，则直接返回，否则，拼接 endpoint 和 bucket
     *
     * @return 拼接后的 domain
     */
    private String buildDomain() {
        // 如果已经是 http 或者 https，则不进行拼接.主要适配 MinIO
        if (HttpUtil.isHttp(config.getEndpoint()) || HttpUtil.isHttps(config.getEndpoint())) {
            return StrUtil.format("{}/{}", config.getEndpoint(), config.getBucket());
        }
        // 阿里云、腾讯云、华为云都适合。七牛云比较特殊，必须有自定义域名
        return StrUtil.format("https://{}/{}", config.getEndpoint(), config.getBucket());
    }

    /**
     * 构建 S3 客户端的 Endpoint 配置，包括 region、endpoint
     *
     * @return S3 客户端的 EndpointConfiguration 配置
     */
    private URI buildEndpointConfiguration() {
        return URI.create(config.getEndpoint());
    }

    /**
     * 基于 config 秘钥，构建 S3 客户端的认证信息
     *
     * @return S3 客户端的认证信息
     */
    private StaticCredentialsProvider buildCredentials() {
        return StaticCredentialsProvider.create(
                AwsBasicCredentials.create(config.getAccessKey(), config.getAccessSecret()));
    }

}
